iT邦幫忙

2023 iThome 鐵人賽

DAY 22
1
Modern Web

深入淺出,完整認識 Next.js 13 !系列 第 22

Day 22 - 功能性路由 ( 二 ):Parallel Routes & Intercepting Routes

  • 分享至 

  • xImage
  •  

假如我們今天要做一個 dashboard,頁面上包含全域共用的 Header,以及 數據分析團隊介紹 兩個區塊,大致 layout 如下:
https://ithelp.ithome.com.tw/upload/images/20230916/20161853Xeu4sIA80j.png

很直覺地,可能會想到建 <Team><Analytics> 兩個 component,再 import 進 app/dashboard/page.tsx 中:

/* app/dashboard/page.tsx */
import Analytics from './components/Analytics';
import Team from './components/Team';

export default function Page() {
  return (
    <>
      <Analytics />
      <Team />
    </>
  );
}

處理 Streaming

假設兩個 components 都是 Server Components,其中一個 component <Team> 處理 data fetching 要 5 秒,就有可能讓使用者進到頁面後,要乾等 5 秒才能看得到畫面。

這時候可能會聯想到 Day 15 分享的內容,透過 <Suspense> component 來實現 streaming,讓頁面其他部分先渲染並顯示,還在處理 data fetching 的部分可以先顯示 Loading...:

/* app/dashboard/page.tsx */
import { Suspense } from 'react';
import Analytics from './components/Analytics';
import Loading from './components/Loading';
import Team from './components/Team';

export default function Page() {
  return (
    <>
      <Analytics />
      <Suspense fallback={<Loading />}>
        <Team />
      </Suspense>
    </>
  );
}

這樣當 <Team> 還在渲染時,layout 和 <Analytics>可以渲然完會先顯示,<Team> 則先顯示 Loading...,渲染完後再替換為 <Team> 的 UI。
suspense demo

處理 Error Boundaries

處理完 Streaming,你可能會接著想到 Day 16 的內容,透過 error boundaries,當其中一個 component 渲染發生問題時,讓其他部分依然可以正常顯示:

import { Suspense } from 'react';
import Analytics from './components/Analytics';
import ErrorBoundary from './components/ErrorBoundary';
import Loading from './components/Loading';
import Team from './components/Team';

export default function Page() {
  return (
    <>
      <ErrorBoundary>
        <Analytics />
      </ErrorBoundary>
      <ErrorBoundary>
        <Suspense fallback={<Loading />}>
          <Team />
        </Suspense>
      </ErrorBoundary>
    </>
  );
}

寫到這邊,突然覺得很熟悉。Day 15 和 Day 16 有提到兩個特殊檔案:loading.tsxerror.tsx,可以讓我們以 route segment 為單位,很簡單地做 streaming 和加 error boundaries。於是我就突發奇想:有辦法讓 <Analytics><Team> 變成兩個類似 route segment 的東西,讓他們可以使用 layout、loading、error 這些特殊檔案,但又不會影響到 URL 嗎?

還真的有,我們可以使用一個特殊的路由設定:Parallel Routing。

Parallel Routing

Parallel Routing 簡單來說,能在不影響 URL 的背景下,讓多個 Page components,render 在同一個 layout 中。

要怎麼使用呢?

  1. 在資料夾名稱前綴加上 @,告訴 Next 這是一個 parallel route,並一樣在資料夾中新增 page.tsx定義這個 route 的 UI。以上述例子,我們可以在 app/dashboard/@analyticsapp/dashboard/@team各加入一個 page.tsx,定義兩者 UI。

  2. 這時 /dashboard 的 layout 中的 props 就會多兩個 key-value - analytics 和 team,分別代表app/dashboard/@analytics/page.tsxapp/dashboard/@team/page.tsx 定義的 UI:

/* app/dashboard/layout.tsx */
export default function Layout(props: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {props.children}
      {props.team}
      {props.analytics}
    </>
  )
}

parallel routing example
( 圖片來源: https://nextjs.org/docs/app/building-your-application/routing/parallel-routes )

  1. analytics 和 team 就會連同 /dashboard/page.tsx 中定義的 UI,顯示在 /dashbaord 中,@team 和 @analytics 不會影響到 URL。

同理,理論上 parallel route 裡也可以使用 loading.tsxerror.tsx 來定義個別的 suspense fallback UI 和 error boundaries:
parallel route with loading and error
( 圖片來源: https://nextjs.org/docs/app/building-your-application/routing/parallel-routes )
parallel route error boundary

為什麼 loading.tsx 和 error.tsx 沒效果?

但為什麼說理論上呢?因為目前 parallel route 的 loading.tsxerror.tsx 似乎有些 bugs,所以吃不到 parallel route 資料夾中兩者的效果。

今年五月有人在 Next 官方 repo 發 issue 反應這個問題,但官方似乎還沒提供解釋,這個 bug 到目前也還沒修復。

針對這個 bug,目前有幾種解法:

  1. 一樣使用 <Suspense> 和自己寫 error boudary components。

  2. loading.tsxerror.tsx 丟進一個 route group 中,比方說 @analytics/(routeGroup)/loading.tsx

這是 Next 共同創辦人之一 Tim Neutkens 提供的方法,我目前實測的確有效,但我看到蠻多國外網友反應這個方法不可行,他也沒有解釋原理,大家可能要自行嘗試看看。


相同的 URL,不同的排版內容

接著來看另一個情境:我們希望某個 route 的排版內容,會依據使用者從哪個 route 過來,而有所不同。

Unsplash 為例,我們到 Unsplash 首頁點選其中一張圖片,並觀察彈跳視窗的網址,和重新整理後會發生什麼事:
unsplash demo

可以發現,點選圖片後,URL 會切換到 /photos/[圖片id],並跳出一個圖片資訊的彈跳視窗。重新整理後,URL 仍然是 /photos/[圖片id],但頁面的內容還排版和彈跳視窗的內容排版不同

假如想製造這樣的效果,就可以使用 intercepting routes。
intercepting routes demo

Intercepting Routes

那要如何使用 intercepting routes 呢?我們可以在 /app 中的資料夾名稱開頭加入 (..) ,來告訴 Next 這是一個 intercepting routes。括號中的 .. 是對應 intercepting routes 之於 route segment 資料夾的相對路徑。

文字有點抽象,來看個範例。比方說我的檔案結構長這樣:

├── feed
│   ├── (..)photo
│   │   └── page.tsx
│   └── page.tsx
└── photo
    └── page.tsx

因為 intercepting routes 是要對應到和 /feed 同一層的 /photo,所以我要在 /feed 的子路由中攔截 /photo,就要使用 (..)

相對路徑的表示方法可以參考官方說明
https://ithelp.ithome.com.tw/upload/images/20230914/20161853paO76ky4Vc.png
( 圖片來源:https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes#convention )

以上述結構,我們來設計一個簡單的實驗:

  1. 在 /feed 頁面中間放一個「查看圖片」按鈕
  2. feed/(..photo)/page.tsx 加入「攔截版本圖片」文字;photo/page.tsx 加入「一般版本圖片」文字
  3. 觀察點擊「查看圖片」後的頁面
  4. 觀察重新整理後的頁面

當點擊「查看圖片」後,URL 會改為 /photo,頁面中央文字是「攔截版本圖片」。
cick 查看圖片

這時重新整理,會發現 URL 一樣為 /photo,但頁面中央文字改為「一般版本圖片」。
refresh the page

所以,當我們希望從 /feed 切換到 /photo 時,/photo 可以「被攔截」,有一個不同於原本的 UI,就可以使用 intercepting routes。
intercepting routes with modal

提醒一下,intercepting routes 只有用 soft navigation 才會有效果。以 App Router 來說,你可以使用前面提到的 <Link>useRouter() 來切換路由,但假如用 window.location.href 或使用者直接輸入網址,intercepting routes 就不會有效果。

以 Intercepting Routes 製作彈窗

以往在控制彈窗出現時機,可能會用一個 state 來控制:

function App() {
  const [isModalVisible, seIsModalVisible] = useState(false);

  return(
      ...
      {isModalVisible && <Modal />}
      ...
  );
}

了解 intercepting 的概念後,我們可以改用「路由切換」的思維來實作一個類似 Unsplash 的簡單彈窗!路由切換對應彈窗功能的邏輯大致是這樣:

  1. 從 /feed 到 /photo:顯示彈窗
  2. /feed -> /photo -> /feed:關閉彈窗
  3. /feed -> /photo -> 重新整理:拜訪 /photo

依照這個邏輯來實作彈窗:

  1. 寫一個彈窗 component - <Modal>,包含
    a. 一個遮罩 <Mask>
    b. 一個關閉彈窗的按鈕 <ExitButton>,點擊後會拜訪 /feed ( 關閉彈窗 )
    c. children props 可以帶入彈窗內容
/* Modal.tsx */
'use client';
import ExitButton from './ExitButton';
import Mask from './Mask';

export default function Modal({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Mask />
      <div className='...'>
        {children}
        <ExitButton />
      </div>
    </>
  );
}
  1. 我們希望使用者從 /feed 到 /photo,會顯示彈窗,彈窗中帶「攔截版本圖片」文字。因次在 feed/(..photo)/page.tsx import Modal 並在 chilren props 帶入我們要的文字:
/* feed/(..)photo/page.tsx */
import Modal from '@/app/components/Modal';

export default function Page() {
  return <Modal>攔截版本圖片</Modal>;
}
  1. 使用 <Link>,讓使用者點擊 /feed 中的「查看圖片」後,會拜訪 /photo,等同打開彈窗:
/* feed/page.tsx */
'use client';
import Link from 'next/link';

export default function Page() {
  return (
    <div className='...'>
      <Link href='/photo'>
        <button className='...'>
          查看圖片
        </button>
      </Link>
    </div>
  );
}
  1. 我們希望重新整理後,URL 會停留在 /photo,但頁面文字改為「一般版本圖片」,所以就在 photo/page.tsx 加入我們要的文字:
/* photo/page.tsx */
export default function Page() {
  return (
    <div className='...'>
      一般版本圖片
    </div>
  );
}

這樣就完成一個簡單的彈窗啦:
intercepting route popup

官方也有提供一份使用 intercepting routes + parallel routes 來實作彈窗的 sample code,有興趣的讀者可參考連結。


最後做個總結:

  1. 希望讓同一頁的 components 可各自使用 layout.tsxloading.tsxerror.tsx,可以透過 parallel routes。
  2. 目前 parallel routes 的特殊檔案有 bugs,可以嘗試把他們丟到一個 route group 中,或直接用 <Suspense><ErrorBoundary> 等 React components 。
  3. 希望可提供不同路徑來源 ( 僅限 soft navigation ) 的使用者不同 UI,可以透過 intercepting routes。
  4. 透過 intercepting routes,我們可以用路由切換的思維來製作彈窗效果。

以上就是今天的內容!App Router 的路由介紹也到這邊告一段落。學會了 App Router 中怎麼使用 Client Components 和 Server Components,以及幾種路由設定、切換的方式,接下來想帶大家再往後端走一點點,探索幾個能讓 Server 再多幫我們一點的忙。

謝謝大家耐心的閱讀,我們明天見!


上一篇
Day 21 - 功能性路由 ( 一 ):Route Group
下一篇
Day 23 - 再多利用 Server 一點點:Route Handler & Server Actions
系列文
深入淺出,完整認識 Next.js 13 !30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言